﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using NHibernate.Cfg;
using NHibernate.Engine;
using NHibernate.Event;
using NHibernate.Persister.Entity;
using NHibernate;
using EfficientlyLazy.PhotoFramer.UserTypes;

namespace EfficientlyLazy.PhotoFramer.Listeners
{
    public class AuditListener : IAuditListener
    {
        private string _dateCreatedPropertyName = string.Empty;
        private string _dateUpdatedPropertyName = string.Empty;
        private bool _dateSettingEnabled;

        private IAuditLogWriter _auditingWriter;
        private bool _auditingWriterEnabled;

        public static void Register(Configuration config, string dateCreatedPropertyName, string dateUpdatedPropertyName)
        {
            Register(config, dateCreatedPropertyName, dateUpdatedPropertyName, null);
        }

        public static void Register(Configuration config, string dateCreatedPropertyName, string dateUpdatedPropertyName, IAuditLogWriter writer)
        {
            IAuditListener listener = new AuditListener();

            if (!string.IsNullOrEmpty(dateCreatedPropertyName) && !string.IsNullOrEmpty(dateUpdatedPropertyName))
            {
                listener.SetDateStampPropertyNames(dateCreatedPropertyName, dateUpdatedPropertyName);
            }

            if (writer != null)
            {
                listener.SetAuditLogWriter(writer);
            }

            config.EventListeners.PostLoadEventListeners = new IPostLoadEventListener[]
                                                                {
                                                                    listener
                                                                };
            config.EventListeners.PreInsertEventListeners = new IPreInsertEventListener[]
                                                                {
                                                                    listener
                                                                };
            config.EventListeners.PostInsertEventListeners = new IPostInsertEventListener[]
                                                                {
                                                                    listener
                                                                };
            config.EventListeners.PreUpdateEventListeners = new IPreUpdateEventListener[]
                                                                {
                                                                    listener
                                                                };
            config.EventListeners.PostUpdateEventListeners = new IPostUpdateEventListener[]
                                                                {
                                                                    listener
                                                                };
            config.EventListeners.PostDeleteEventListeners = new IPostDeleteEventListener[]
                                                                {
                                                                    listener
                                                                };
        }

        public AuditListener SetDateStampPropertyNames(string dateCreatedPropertyName, string dateUpdatedPropertyName)
        {
            _dateCreatedPropertyName = dateCreatedPropertyName;
            _dateUpdatedPropertyName = dateUpdatedPropertyName;

            _dateSettingEnabled = true;

            return this;
        }

        public AuditListener SetAuditLogWriter(IAuditLogWriter writer)
        {
            _auditingWriter = writer;

            _auditingWriterEnabled = true;

            return this;
        }

        private void AuditRead(Type type, object id)
        {
            if (!_auditingWriterEnabled || _auditingWriter == null)
            {
                return;
            }

            IAuditLogEntry entry = new AuditLogEntry
                                      {
                                          AuditType = AuditingType.Read,
                                          CurrentState = string.Empty,
                                          EntityType = type,
                                          Id = id.ToString(),
                                          OldState = string.Empty,
                                          PropertyName = string.Empty
                                      };

            _auditingWriter.Write(entry);
        }

        private void AuditCreate(Type type, object id)
        {
            if (!_auditingWriterEnabled || _auditingWriter == null)
            {
                return;
            }

            IAuditLogEntry entry = new AuditLogEntry
                                      {
                                          AuditType = AuditingType.Create,
                                          CurrentState = string.Empty,
                                          EntityType = type,
                                          Id = id.ToString(),
                                          OldState = string.Empty,
                                          PropertyName = string.Empty
                                      };

            _auditingWriter.Write(entry);
        }

        private void AuditUpdate(object entity, object id, object[] oldState, object[] currentState, IEntityPersister persister, ISessionImplementor session)
        {
            if (!_auditingWriterEnabled || _auditingWriter == null)
            {
                return;
            }

            int[] dirtyIdxs = persister.FindDirty(currentState, oldState, entity, session);

            List<IAuditLogEntry> entries = new List<IAuditLogEntry>();

            for (int i = 0; i < dirtyIdxs.Length; i++)
            {
                int idx = dirtyIdxs[i];

                if (persister.PropertyTypes[idx].Name == typeof(Encrypted<>).Name)
                {
                    entries.Add(new AuditLogEntry
                                    {
                                        CurrentState = "<Encrypted>",
                                        Id = id.ToString(),
                                        OldState = "<Encrypted>",
                                        PropertyName = persister.PropertyNames[idx],
                                        EntityType = entity.GetType(),
                                        AuditType = AuditingType.Update
                                    });

                    continue;
                }

                string propName = persister.PropertyNames[idx];
                if (_dateSettingEnabled && (propName.Equals(_dateCreatedPropertyName, StringComparison.CurrentCulture) || propName.Equals(_dateUpdatedPropertyName, StringComparison.CurrentCulture)))
                {
                    Debug.WriteLine(string.Format("UPDATE OMIT: {0} - {1} - {2}", propName, entity, id));
                    continue;
                }

                IAuditLogEntry entry = new AuditLogEntry
                                       {
                                           CurrentState = currentState[idx].ToString(),
                                           Id = id.ToString(),
                                           OldState = oldState[idx].ToString(),
                                           PropertyName = persister.PropertyNames[idx],
                                           EntityType = entity.GetType(),
                                           AuditType = AuditingType.Update
                                       };

                entries.Add(entry);
            }

            _auditingWriter.Write(entries);
        }

        private void AuditDelete(Type type, object id)
        {
            if (!_auditingWriterEnabled || _auditingWriter == null)
            {
                return;
            }

            IAuditLogEntry entry = new AuditLogEntry
                                      {
                                          AuditType = AuditingType.Delete,
                                          CurrentState = string.Empty,
                                          EntityType = type,
                                          Id = id.ToString(),
                                          OldState = string.Empty,
                                          PropertyName = string.Empty
                                      };

            _auditingWriter.Write(entry);
        }

        private void SetUpdateDateStamps(object entity, object[] state, IEntityPersister persister)
        {
            if (!_dateSettingEnabled || string.IsNullOrEmpty(_dateUpdatedPropertyName))
            {
                return;
            }

            Set(entity, persister, state, _dateUpdatedPropertyName, DateTime.Now);
        }

        private void SetCreateDateStamps(object entity, object[] state, IEntityPersister persister)
        {
            if (!_dateSettingEnabled || string.IsNullOrEmpty(_dateUpdatedPropertyName) || string.IsNullOrEmpty(_dateCreatedPropertyName))
            {
                return;
            }

            DateTime created = DateTime.Now;

            Set(entity, persister, state, _dateCreatedPropertyName, created);
            Set(entity, persister, state, _dateUpdatedPropertyName, created);
        }

        private static void Set(object entity, IEntityPersister persister, object[] state, string propertyName, object value)
        {
            var index = Array.IndexOf(persister.PropertyNames, propertyName);

            if (index == -1)
            {
                return;
            }

            EntityMode? mode = persister.GuessEntityMode(entity);

            persister.SetPropertyValue(entity, index, value, mode.Value);

            state[index] = value;
        }

        public virtual void OnPostLoad(PostLoadEvent @event)
        {
            AuditRead(@event.Entity.GetType(), @event.Id);
        }

        public virtual bool OnPreInsert(PreInsertEvent @event)
        {
            SetCreateDateStamps(@event.Entity, @event.State, @event.Persister);

            return false;
        }

        public virtual void OnPostInsert(PostInsertEvent @event)
        {
            AuditCreate(@event.Entity.GetType(), @event.Id);
        }

        public virtual bool OnPreUpdate(PreUpdateEvent @event)
        {
            SetUpdateDateStamps(@event.Entity, @event.State, @event.Persister);

            return false;
        }

        public virtual void OnPostUpdate(PostUpdateEvent @event)
        {
            AuditUpdate(@event.Entity, @event.Id, @event.OldState, @event.State, @event.Persister, @event.Session);
        }

        public virtual void OnPostDelete(PostDeleteEvent @event)
        {
            AuditDelete(@event.Entity.GetType(), @event.Id);
        }
    }
}
